講了這麼多,也該是要造個輪子練練手感了。事實上使用 Vue 等前端框架,最主要就是 component 可以重複利用,所以練手感歸練手感,如果沒有太多的 UI/UX 需求,可以多多使用開源 components,將時間放在刀口上 (例如癱在床上吸個貓 XD)。另外因為接下來是手把手造輪子,所以各位鐵人大大如果覺得囉嗦,可以直接到這裡看完整的 code!
在大多數的系統中都會有表單的「新增」、「修改」和「檢視」三種模式,今天要來寫個 component 讓我們只要痛苦一次,之後各種好用 (我們會用到部分 iview
的 components) !
規劃一下,form 上的每個欄位,基本上會有:
所以我們在 component
建立 Field.vue
。
export default {
name: 'field',
props: {
label: { type: String, default: '' },
type: { type: String, default: 'text' },
value: { required: true },
errorMessages: { type: Array, default: () => { return []; } },
},
data() {
return {
// 由於我們欄位值會變動,所以要把 props 中的 value 賦值到 data 中的屬性
fieldValue: this.value,
};
}
}
<template>
當中 (這裡以 text, password, select, datetime 作為範例)。<template>
<div>
<!-- text -->
<template v-if="type === 'text'">
<input type="text" v-model="fieldValue" />
</template>
<!-- password -->
<template v-if="type === 'password'">
<input type="password" v-model="fieldValue" />
</template>
<!-- select -->
<template v-if="type === 'select'">
<select v-model="fieldValue">
</select>
</template>
<!-- datetime -->
<template v-if="type === 'datetime'">
<DatePicker type="date" v-model="fieldValue">
</DatePicker>
</template>
</div>
</template>
Field.vue
增加 options
屬性。export default {
name: 'field',
props: {
// ...
options: { type: Object, default: () => { return {}; }},
},
// ...
}
options
補齊 <template>
裡的資料<template>
<div>
<!-- ... -->
<!-- select -->
<template v-if="type === 'select'">
<select v-model="fieldValue">
<option v-for="(item, index) in options.selectOptions" :key="index"
:value="item.value">
{{item.label}}
</option>
</select>
</template>
<!-- datetime -->
<template v-if="type === 'datetime'">
<DatePicker :type="options.type || 'date'"
v-model="fieldValue" :format="options.formate">
</DatePicker>
</template>
</div>
</template>
fieldValue
製作 v-model 雙向綁定。export default {
// ...
watch: {
fieldValue() {
this.inputEmit();
}
},
methods: {
inputEmit() {
this.$emit('input', this.fieldValue);
},
}
}
<template>
<div>
<div>
<div class="label inline-block">{{label}}</div>
<div class="field inline-block">
<!-- text ... -->
<!-- password ... -->
<!-- select ... -->
<!-- datetime ... -->
</div>
<div>
<ul>
<li v-for="(message, index) in errorMessages" :key="index">
{{message}}
</li>
</ul>
</div>
</div>
</template>
page
底下新增 user/_id/_type/index.vue
來測試看看吧!<template>
<div>
<div class="form">
<field v-for="(field, key) in registerForm.schema" :key="key"
:label="field.label"
:type="field.type"
v-model="registerForm.data[key]"
:errorMessages="registerForm.errorMessages[key]"
:options="field.options">
</field>
</div>
</div>
</template>
<script>
import Field from '../components/Field';
export default {
name: 'index',
components: {Field},
data() {
return {
registerForm: undefined,
};
},
async asyncData() {
const registerForm = {
schema: {
name: { label: '姓名', type: 'text' },
email: { label: 'Email', type: 'text' },
password: { label: '密碼', type: 'password' },
role: { label: '角色', type: 'select', options: {
selectOptions: [
{ label: '管理者', value: 0 },
{ label: '一般使用者', value: 1 },
],
} },
effectiveAt: { label: '生效日期', type: 'datetime' },
},
data: {
name: undefined,
email: undefined,
password: undefined,
role: 1,
effectiveAt: undefined,
},
errorMessages: {},
};
return { registerForm };
}
}
</script>
至目前為止,透過 Field.vue
我們可以輕鬆透過資料快速建立表單,完成「新增」、「修改」,接下來再調整一下 Field.vue
props
中加入 edit
屬性:export default {
// ...
props: {
// ...
edit: { type: Boolean, default: true }
}
// ...
}
<template>
中,透過 v-if 增加顯示文字的部分:<template>
<!-- ... -->
<div class="field inline-block">
<template v-if="edit">
<!-- text ... -->
<!-- password ... -->
<!-- select ... -->
<!-- datetime ... -->
</template>
<template v-else>
<span>{{ fieldValue }}</span>
</template>
</div>
</template>
回到 user/_id/_type/index.vue
,我們根據 _id
與 _type
,來調整表單是用來「新增」、「修改」或是「檢視」
<template>
<div>
<div class="form">
<field v-for="(field, key) in registerForm.schema" :key="key"
:label="field.label"
:type="field.type"
v-model="registerForm.data[key]"
:errorMessages="registerForm.errorMessages[key]"
:options="field.options"
:edit="toEdit">
</field>
</div>
</div>
</template>
<script>
import Field from '~/components/Field';
// 模擬用的 API
const simulateAPI = (response, success = true) => {
return new Promise((resolve, rejected) => {
if (success) {
resolve(response);
} else {
rejected(response);
}
});
};
export default {
// ...
async asyncData({ params, redirect }) {
// 從 route 上面取得的參數 id 與 type
const userId = params['id'];
const type = params['type'];
const toEdit = type === 'create' || type === 'edit';
const getUserData = type === 'edit' || type === 'view';
const registerForm = {
schema: {
// 移除 password 欄位,有需要再加上來即可
// password: { label: '密碼', type: 'password' },
},
// ...
};
if (getUserData) {
try {
// 模擬呼叫 API 取得 data:
const expectedResponse = {
name: 'albert',
email: 'account@test.org',
role: 1,
effectiveAt: '2019-09-27',
};
const allResponses = await Promise.all([
simulateAPI(expectedResponse),
]);
registerForm.data = allResponses[0];
} catch (error) {
redirect('/');
}
} else {
// 「新增表單」(註冊表單) 才需要用到
registerForm.schema.password = { label: '密碼', type: 'password' };
registerForm.schema.password_confirmation = { label: '確認密碼', type: 'password' };
}
return { registerForm, userId, toEdit };
}
}
</script>
上面的範例現在只要透過 route 的改變,即可快速轉換成「新增」、「修改」和「檢視」三種不同狀態的表單,而 page component 就專注於取得資料即可。如果在系統中有各種表單資料需要填寫,透過 Field.vue
component 就可以快速建置好三種不同類型的頁面。
Field.vue
這樣的寫法雖然方便但也是有缺點:
角色
欄位的值是顯示 1
,這個部分 Field.vue
需要再依各個不同欄位類型,進行顯示調整。Field.vue
中,同時又包含「檢視」模式,雖然後續方便好使用,但在維護的部分可能就比較辛苦 (尤其時間久了之後,大概就只有上帝看得懂了),所以如何拆分、重構,就等到系統需求明確之後再重新考量。手把手寫範例果然又臭又長阿 XD,各位鐵人大大如果覺得麻煩可以直接到這裡看完整 code 就好。 今天表單範例就是讓大家練練手感,感覺一下使用 component 組合頁面是多麼愜意的事。在實際的系統開發上,是不是要使用開源的 components 或是有必要自己造輪子 (Field.vue
) 還是老話一句:「視需求而定」!
明天沒意外會寫個完整的登入、登出以及透過 middleware 保護瀏覽頁面的小專案 (希望不要冨樫 ><" ),那就先這樣啦!